﻿using System.Xml;
using System;
using System.Reflection;
using System.CodeDom;
using System.IO;
using System.Collections.Generic;
using System.Xml.Xsl;
using System.Text;
using OrganizationName.CodeGeneration;
using OrganizationName.Common;
using OrganizationName.Runtime;

namespace OrganizationName.Factory
{
    public class RuntimeConfiguration : CodeProvider, IConfiguredThroughXml
    {
        public virtual Type[] Type { get; set; }
        public virtual Instance[] Instance { get; set; }
        public virtual Invoke[] Invoke { get; set; }

        public virtual Type GetTypeByName(string typeName)
        {
            if (string.IsNullOrEmpty(typeName))
            {
                throw new ArgumentNullException();
            }

            foreach (Type type in Type)
            {
                if (type.Name == typeName)
                {
                    return type;
                }
            }
            throw new ArgumentException("No Type could be found with Name = [" + typeName + "]");
        }

        public virtual AnonymousInstance GetInstanceByBaseTypeName(string baseTypeName)
        {
            if (string.IsNullOrEmpty(baseTypeName))
            {
                throw new ArgumentNullException();
            }

            foreach (Instance instance in Instance)
            {
                if (instance.Base == baseTypeName)
                {
                    return instance;
                }
            }

            foreach (Invoke invoke in Invoke)
            {
                if (invoke.Base == baseTypeName)
                {
                    return invoke;
                }
            }

            throw new ArgumentException("No Instance/Invoke could be found with Base = [" + baseTypeName + "]");
        }

        public virtual AnonymousInstance GetInstanceByName(string instanceName)
        {
            if (string.IsNullOrEmpty(instanceName))
            {
                throw new ArgumentNullException();
            }

            foreach (Instance instance in Instance)
            {
                if (instance.Name == instanceName)
                {
                    return instance;
                }
            }

            foreach (Invoke invoke in Invoke)
            {
                if (invoke.Name == instanceName)
                {
                    return invoke;
                }
            }

            throw new ArgumentException("No Instance/Invoke could be found with Name = [" + instanceName + "]");
        }

        public virtual void Configure(Stream xmlStream)
        {
            using (XmlTextReader xmlReader = new XmlTextReader(xmlStream))
            {
                System.Xml.XmlDocument doc = new XmlDocument();
                doc.Load(xmlReader);
                ParseXmlNode(doc.DocumentElement, this);
            }
        }

        public virtual void Configure(string xmlFilePath)
        {
            using (FileStream xmlStream = File.Open(xmlFilePath, FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                Configure(xmlStream);
            }
        }

        public virtual void Configure(string xmlFilePath, string xslFilePath)
        {
            using (Stream formattedXmlStream = Utility.CompileXml(xmlFilePath, xslFilePath))
            {
                Configure(formattedXmlStream);
            }
        }

        private CodeProvider ParseXmlNode(XmlNode node, CodeProvider instance)
        {
            System.Type type;

            if (instance == null)
            {
                type = Assembly.GetExecutingAssembly().GetType(typeof(RuntimeConfiguration).Namespace + "." + node.Name);
                instance = Activator.CreateInstance(type) as CodeProvider;
                if (instance == null)
                {
                    throw new FactoryConfigurationException("The type " + type + " does not extend " + typeof(CodeProvider), node, null);
                }
            }
            else
            {
                type = instance.GetType();
            }

            XmlNode currentNode = null;

            try
            {
                foreach (XmlAttribute attribute in node.Attributes)
                {
                    currentNode = attribute;

                    PropertyInfo propertyInfo = type.GetProperty(attribute.Name);
                    if (propertyInfo != null)
                    {
                        propertyInfo.SetValue(instance, Convert.ChangeType(attribute.Value, propertyInfo.PropertyType), null);
                    }
                    else
                    {
                        throw new FactoryConfigurationException("The type " + type + " does not contain a property with the name " + attribute.Name, node, null);
                    }
                }

                foreach (XmlNode testNode in node.ChildNodes)
                {
                    XmlElement element = testNode as XmlElement;

                    if (element == null)
                    {
                        continue;
                    }

                    currentNode = element;

                    PropertyInfo propertyInfo = type.GetProperty(element.Name);
                    if (propertyInfo != null)
                    {
                        CodeProvider newInstance = ParseXmlNode(element, null);
                        newInstance.Parent = instance;
                        instance.Children.Add(newInstance);

                        if (propertyInfo.PropertyType.IsArray)
                        {
                            Array array = propertyInfo.GetValue(instance, null) as Array;
                            if (array == null)
                            {
                                array = Array.CreateInstance(propertyInfo.PropertyType.GetElementType(), element.ParentNode.SelectNodes(element.Name).Count);
                                propertyInfo.SetValue(instance, array, null);
                            }
                            array.SetValue(newInstance, Utility.GetIndexOfXmlNodeInParent(element));
                        }
                        else
                        {
                            if (element.ParentNode.SelectNodes(element.Name).Count > 1)
                            {
                                throw new FactoryConfigurationException("The child " + element.Name + " cannot be specified more than once", currentNode, null);
                            }
                            propertyInfo.SetValue(instance, newInstance, null);
                        }
                    }
                    else
                    {
                        throw new FactoryConfigurationException("The type " + type + " does not contain a settable property with the name " + element.Name, currentNode, null);
                    }
                }
                return instance;
            }
            catch (FactoryConfigurationException)
            {
                throw;
            }
            catch (Exception unknownError)
            {
                throw new FactoryConfigurationException("An exception has been thrown while loading runtime configuration", currentNode, unknownError);
            }
        }

        private CodeMemberMethod GenerateMethodDeclaration(string methodName, System.Type returnType, System.Type[] variableTypes, string[] variableNames)
        {
            CodeMemberMethod createMethod = new CodeMemberMethod();
            createMethod.Name = methodName;
            createMethod.Attributes = MemberAttributes.Public | MemberAttributes.Override;
            for (int i = 0; i < variableTypes.Length; i++)
            {
                createMethod.Parameters.Add(new CodeParameterDeclarationExpression(variableTypes[i], variableNames[i]));
            }
            createMethod.ReturnType = new CodeTypeReference(returnType);
            return createMethod;
        }

        private List<AnonymousInstance> GetAllCreatableObjects()
        {
            List<AnonymousInstance> allCreatables = new List<AnonymousInstance>();
            if (Instance != null)
            {
                allCreatables.AddRange(Instance);
            }
            if (Invoke != null)
            {
                allCreatables.AddRange(Invoke);
            }
            return allCreatables;
        }

        private void GenerateMethodImplementation(ICodeContext codeContext, bool isQueryMethod, bool isByTypeMethod)
        {
            System.Type tType = typeof(System.Type);
            System.Type tString = typeof(string);
            System.Type tBool = typeof(bool);
            System.Type tObject = typeof(object);

            System.Type tParameter = isByTypeMethod ? tType : tString;
            string nParameter = isByTypeMethod ? DynamicFactory.FACTORY_METHOD_PARAMETER_SERVICE_TYPE : DynamicFactory.FACTORY_METHOD_PARAMETER_SERVICE_NAME;

            CodeMemberMethod createMethod = GenerateMethodDeclaration
            (
                isQueryMethod ? DynamicFactory.FACTORY_EXISTS_METHOD_NAME : DynamicFactory.FACTORY_GET_METHOD_NAME,
                isQueryMethod ? tBool : tObject,
                isQueryMethod ? new System.Type[] { tParameter, tType } : new System.Type[] { tParameter },
                isQueryMethod ? new string[] { nParameter, DynamicFactory.FACTORY_METHOD_PARAMETER_EXPECTED_TYPE } : new string[] { nParameter }
            );

            foreach (AnonymousInstance instance in GetAllCreatableObjects())
            {
                Type baseType = null;
                ICodeContext instanceContext = codeContext.CreateChild(instance, true);
                if (isByTypeMethod)
                {
                    if (!string.IsNullOrEmpty(instance.Base))
                    {
                        baseType = (codeContext.RootCodeProvider as RuntimeConfiguration).GetTypeByName(instance.Base);
                        if (baseType == null)
                        {
                            throw new CodeGenerationException("The base type [" + instance.Base + "] could not be found", instanceContext, null);
                        }
                    }
                }

                bool eligibilityTest = isByTypeMethod ? !string.IsNullOrEmpty(instance.Base) : !string.IsNullOrEmpty(instance.Name);
                if (eligibilityTest)
                {
                    createMethod.Statements.Add
                    (
                        new CodeConditionStatement
                        (
                            new CodeBinaryOperatorExpression
                            (
                                new CodeVariableReferenceExpression(nParameter),
                                CodeBinaryOperatorType.IdentityEquality,
                                isByTypeMethod
                                ?
                                    (CodeExpression)new CodeTypeOfExpression
                                    (
                                        new CodeTypeReference(baseType.GetConcreteType(codeContext.CreateChild(baseType)))
                                    )
                                :
                                    (CodeExpression)new CodeSnippetExpression("\"" + instance.Name + "\"")
                            ),
                            new CodeMethodReturnStatement
                            (
                                isQueryMethod
                                ?
                                    GetExpectedTypeConditionalExpression(instance.GetConcreteType(instanceContext), DynamicFactory.FACTORY_METHOD_PARAMETER_EXPECTED_TYPE)
                                :
                                    instance.ToRightHandSideExpression(instanceContext)
                            )
                        )
                    );
                }
            }

            if (isQueryMethod)
            {
                createMethod.Statements.Add(new CodeMethodReturnStatement(new CodePrimitiveExpression(false)));
            }
            else
            {
                createMethod.Statements.Add
                (
                    new CodeThrowExceptionStatement
                    (
                        new CodeObjectCreateExpression
                        (
                            new CodeTypeReference(typeof(System.ArgumentException)),
                            new CodeSnippetExpression("\"Service could not be found\"")
                        )
                    )
                );
            }

            codeContext.Class.Members.Add(createMethod);
        }

        private CodeExpression GetExpectedTypeConditionalExpression(System.Type concreteType, string variableName)
        {
            System.Type[] baseTypesAndInterfaces = GetAllBaseTypesAndInterfaces(concreteType);
            CodeBinaryOperatorExpression orStatement = null;

            for (int i = 0; i < baseTypesAndInterfaces.Length; i++)
            {
                CodeBinaryOperatorExpression appendingOrStatement = new CodeBinaryOperatorExpression();
                appendingOrStatement.Left = new CodeVariableReferenceExpression(variableName);
                appendingOrStatement.Operator = CodeBinaryOperatorType.IdentityEquality;
                appendingOrStatement.Right = new CodeTypeOfExpression(baseTypesAndInterfaces[i]);

                if (orStatement == null)
                {
                    orStatement = appendingOrStatement;
                }
                else
                {
                    CodeBinaryOperatorExpression finalOrStatement = new CodeBinaryOperatorExpression();
                    finalOrStatement.Left = orStatement;
                    finalOrStatement.Operator = CodeBinaryOperatorType.BooleanOr;
                    finalOrStatement.Right = appendingOrStatement;

                    orStatement = finalOrStatement;
                }
            }

            return orStatement;
        }

        private System.Type[] GetAllBaseTypesAndInterfaces(System.Type concreteType)
        {
            List<System.Type> typeList = new List<System.Type>();

            System.Type baseType = concreteType;

            while (baseType != null)
            {
                typeList.Add(baseType);
                baseType = baseType.BaseType;
            }

            typeList.AddRange(concreteType.GetInterfaces());

            return typeList.ToArray();
        }

        public override void DeclarationPhase(ICodeContext codeContext)
        {
        }

        public override void InitializationPhase(ICodeContext codeContext)
        {
            GenerateMethodImplementation(codeContext, false, false);
            GenerateMethodImplementation(codeContext, false, true);
            GenerateMethodImplementation(codeContext, true, false);
            GenerateMethodImplementation(codeContext, true, true);
        }

        public override void FinalizationPhase(ICodeContext codeContext)
        {
            if (Instance != null)
            {
                foreach (Instance instance in Instance)
                {
                    instance.GenerateCode(codeContext.CreateChild(instance, true));
                }
            }

            if (Invoke != null)
            {
                foreach (Invoke invoke in Invoke)
                {
                    invoke.GenerateCode(codeContext.CreateChild(invoke, true));
                }
            }
        }

        public override CodeExpression ToRightHandSideExpression(ICodeContext codeContext)
        {
            throw new NotImplementedException();
        }
    }
}
